package com.lemon.exam.service.impl;

import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.crud.BaseServiceImpl;
import com.lemon.exam.common.entity.view.ViewClassGradeUserJob;
import com.lemon.exam.common.entity.vo.SSEMessageVO;
import com.lemon.exam.common.entity.vo.TreeSelectVO;
import com.lemon.exam.common.enums.ServiceEnum;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.properties.ConfigProperties;
import com.lemon.exam.common.service.FileService;
import com.lemon.exam.common.util.PaperUtil;
import com.lemon.exam.common.util.SecurityUtil;
import com.lemon.exam.common.web.Param;
import com.lemon.exam.common.web.Request;
import com.lemon.exam.entity.po.PaperPO;
import com.lemon.exam.entity.vo.PaperQuestionVO;
import com.lemon.exam.entity.vo.PaperVO;
import com.lemon.exam.repository.PaperRepository;
import com.lemon.exam.service.IClassService;
import com.lemon.exam.service.IPaperService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

/**
 * 试卷表 服务实现类
 *
 * @author Lemon
 * @since 2025/05/11 13:47:13
 */
@Slf4j
@Service
public class PaperServiceImpl extends BaseServiceImpl<PaperPO, Long, PaperRepository> implements IPaperService {

    //试卷模板文件路径
    private static final String PAPER_TEMPLATE_NAME = "试卷模板.xlsx";
    @Resource
    private FileService fileService;
    @Resource
    private IClassService classService;
    @Resource
    private ConfigProperties configProperties;

    @Override
    public Mono<Map<String, Object>> upload(FilePart filePart) {
        return Mono.zip(
                        fileService.uploadToLocal(filePart, "paper"),
                        PaperUtil.parseExcelAndGetColumnData(filePart, 2, 5).collectList().map(scores -> scores.parallelStream().mapToDouble(Double::parseDouble).sum())
                ).map(tuple -> Map.<String, Object>of(
                        "originFileName", tuple.getT1().originName(),
                        "newFileName", tuple.getT1().newName(),
                        "filePath", tuple.getT1().path(),
                        "totalScore", tuple.getT2()
                ))
                .onErrorResume(e -> {
                    log.error("文件上传或解析失败", e);
                    return Mono.error(new CustomException(500, "文件上传或解析失败：%s".formatted(e.getMessage())));
                });
    }

    @Override
    public Flux<TreeSelectVO> findClassSelect() {
        return classService.findAllClassGrade()
                .collectList()
                .flatMapMany(this::buildClassTree)
                .subscribeOn(Schedulers.parallel());
    }

    /**
     * 构建班级树
     *
     * @param list
     * @return
     */
    private Flux<TreeSelectVO> buildClassTree(List<ViewClassGradeUserJob> list) {
        if (CollectionUtils.isEmpty(list)) {
            return Flux.empty();
        }

        //按grade_id分组 构建树
        return Flux.fromIterable(list.stream()
                .collect(Collectors.groupingBy(ViewClassGradeUserJob::getGradeId))
                .entrySet()
                .stream()
                .map(entry -> new TreeSelectVO(
                        entry.getKey(),
                        entry.getValue().get(0).getGradeName(),
                        false,
                        entry.getValue().stream().map(item -> new TreeSelectVO(
                                item.getClassId(),
                                item.getClassName(),
                                false,
                                null
                        )).toList()))
                .toList());
    }

    @Override
    public Mono<Void> exportTemplate(ServerHttpResponse response) {
        return Mono.fromCallable(() -> new ClassPathResource("examination/" + PAPER_TEMPLATE_NAME).getFile())
                .flatMap(file -> {
                    // URL编码中文文件名
                    String filename = URLEncoder.encode(PAPER_TEMPLATE_NAME, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
                    return ((ZeroCopyHttpOutputMessage) setHeader.apply(response, filename)).writeWith(file, 0, file.length());
                }).subscribeOn(Schedulers.parallel());
    }

    /**
     * 设置响应头
     */
    private static final BiFunction<ServerHttpResponse, String, ServerHttpResponse> setHeader = (response, filename) -> {
        response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", filename, filename));
        response.getHeaders().setContentType(ApiConst.EXCEL_MEDIA_TYPE);
        return response;
    };

    @Override
    public Mono<Void> download(Long id, ServerHttpResponse response) {
        return repository.findFileNameFilePathById(id)
                .flatMap(paper -> {
                    File file = new File(configProperties.getUploadPath(paper.getFilePath()));
                    String filename = URLEncoder.encode(paper.getOriginFileName(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
                    return ((ZeroCopyHttpOutputMessage) setHeader.apply(response, filename)).writeWith(file, 0, file.length());
                }).subscribeOn(Schedulers.parallel());
    }

    @Override
    public Mono<List<PaperQuestionVO>> findPaperQuestion(Long id) {
        return repository.findFileNameFilePathById(id)
                .map(paper -> configProperties.getUploadPath(paper.getFilePath()))
                .flatMap(PaperUtil::getPaperQuestion);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Mono<Page<?>> page(Param param) {
        return Mono.zip(super.page(param), param.getPageRequest()).flatMap(tuple -> {
            List<PaperPO> content = (List<PaperPO>) tuple.getT1().getContent();
            if (CollectionUtils.isEmpty(content)) {
                return Mono.just(tuple.getT1());
            }
            List<Long> classIdList = content.parallelStream().map(PaperPO::getClassId).distinct().toList();
            return classService.findClassNameGradeNameById(classIdList).map(classGrade -> {
                content.forEach(item -> item.setClassName(classGrade.get(item.getClassId())));
                return new PageImpl<>(content, tuple.getT2(), tuple.getT1().getTotalElements());
            });
        });
    }

    @Override
    public Mono<String> insert(PaperPO param) {
        return SecurityUtil.getLoginUser()
                .flatMap(user -> {
                    param.setCreateId(user.getId());
                    param.setCreateBy(user.getUsername());
                    return super.insert(param);
                });
    }

    @Override
    public Mono<String> update(PaperPO param) {
        return Mono.zip(
                SecurityUtil.getUserId(),
                repository.findCreateIdAndStartTimeById(param.getId())
        ).flatMap(tuple -> {
            if (!tuple.getT2().getCreateId().equals(tuple.getT1())) {
                return Mono.error(new CustomException(403, "非创建人无法修改！"));
            } else if (tuple.getT2().getEndTime().isAfter(LocalDateTime.now())) {
                return Mono.error(new CustomException(403, "考试已结束无法修改！"));
            } else if (tuple.getT2().getStartTime().isBefore(LocalDateTime.now())) {
                return Mono.error(new CustomException(403, "考试已开始无法修改！"));
            } else {
                return super.update(param);
            }
        });
    }

    @Override
    public Flux<PaperVO> findPaddingPaperByAnswerUser(int pageNum, int pageSize) {
        return SecurityUtil.getUserId().flatMapMany(userId -> repository.findPaddingAnswerPaperByPage(userId, pageNum * pageSize, pageSize));
    }

    @Override
    public Flux<PaperVO> findFinishPaperByAnswerUser(int pageNum, int pageSize) {
        return SecurityUtil.getUserId().flatMapMany(userId -> repository.findFinishAnswerPaperByPage(userId, pageNum * pageSize, pageSize));
    }

    @Override
    public Flux<PaperVO> findNotFillPaperByAnswerUser(int pageNum, int pageSize) {
        return SecurityUtil.getUserId().flatMapMany(userId -> repository.findNotFillAnswerPaperByPage(userId, pageNum * pageSize, pageSize));
    }

    @Override
    public Flux<PaperVO> findPaddingPaperByGradingUser(int pageNum, int pageSize) {
        return SecurityUtil.getUserId().flatMapMany(userId -> repository.findPaddingGradingPaperByPage(userId, pageNum * pageSize, pageSize));
    }

    @Override
    public Flux<PaperVO> findFinishPaperByGradingUser(int pageNum, int pageSize) {
        return SecurityUtil.getUserId().flatMapMany(userId -> repository.findFinishGradingPaperByPage(userId, pageNum * pageSize, pageSize));
    }

    @Override
    public Mono<PaperPO> findPaperById(Long id) {
        return repository.findPaperById(id);
    }

    @Override
    public Mono<LocalDateTime> findEndTimeById(Long id) {
        return repository.findEndTimeById(id).map(PaperPO::getEndTime);
    }

    @Override
    public Mono<PaperPO> findPassScoreFilePathByAnswerId(Long answerId) {
        return repository.findPassScoreFilePathByAnswerId(answerId);
    }

    @Override
    public Flux<SSEMessageVO> generatePaper(Map<String, Object> param, String token) {
        return Request.sse(
                ServiceEnum.GENERATE_PAPER,
                "/generate_paper",
                param,
                headers -> headers.set(HttpHeaders.AUTHORIZATION, token),
                SSEMessageVO.class);
    }
}
